package com.fengye.common.aspectj;

import com.alibaba.fastjson.JSON;
import com.fengye.common.annotation.Log;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import java.lang.annotation.Annotation;

/**
 * @author sunlei
 */
@Slf4j
@Aspect // 将当前类标记为切面类
@Component // 将当前类的对象创建使用维护交由spring容器维护
public class LogAspect {

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.fengye.common.annotation.Log)")
    public void pt() {
    }

    /**
     * 打印某个方法的详细日志
     * @param pjp pjp
     * @return 结果
     */
    @Around("pt()")
    public Object printLog(ProceedingJoinPoint pjp) throws Throwable {

        Object result = null; // 调用目标方法：例如login方法
        String afterLog = null;
        String beforeLog = null;
        try { // 注意此处不能catch异常，如果此处catch了方法的异常，那全局异常处理就无法捕捉到异常了
            beforeLog = printLogBefore(pjp);
            result = pjp.proceed();
            afterLog = printLogAfter(result);
        } finally {
            log.info(beforeLog + afterLog);
        }

        return result;
    }

    private String printLogBefore(ProceedingJoinPoint pjp) {
        // 获取方法签名
        MethodSignature methodSignature = getMethodSignature(pjp);
        // 获取当前请求的request对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert requestAttributes != null;
        HttpServletRequest request = requestAttributes.getRequest();

        // 获取被增强方法上的注解对象
        Log printSysLog = getAnnotation(pjp, Log.class);

        return String.format("\n\n=================Log Start================\n" +
                        "URL                ：%s\n" +
                        "HTTP Method        ：%s\n" +
                        "Class Method       ：%s.%s\n" +
                        "IP                 ：%s\n" +
                        "Request Args       ：%s\n",
                request.getRequestURL(),
                request.getMethod(),
                methodSignature.getDeclaringTypeName(),
                methodSignature,
                request.getRemoteHost(),
                JSON.toJSONString(pjp.getArgs()));
    }

    private String printLogAfter(Object obj) {
        // 打印响应结果
        return String.format("Response           ：\n%s\n" +
                        "===============Log End===============%s",
                JSON.toJSONString(obj, true),
                System.lineSeparator());
    }

    /**
     * 打印service/impl下的所有方法的耗时
     *
     * 一、切面方法的要求：
     * 1、切面方法的访问修饰符必须是public
     * 2、切面方法的返回值可以是void和Object，如果这个切面方法被@Around注解修饰，则此切面方法的返回值必须为Object类型，否则随意。
     * 3、切面方法的方法名称可以自定义。
     * 4、切面方法可以接收参数，参数是ProceedingJoinPoint接口类型的参数，但是@Around所修饰的切面方法必须要传递这个参数，否则随意。
     *
     * 二、ProceedingJoinPoint：接口表示连结点，连结点即目标方法的对象
     *
     * 三、execution参数为方法的签名，结构：方法返回值 方法路径(参数)，可以用通配符星号；
     * 形如：List com.kld.quban.service.impl.CartServiceImpl.findVOByUidAndCid(Integer,Integer[])
     */
    @Around("execution(* com.fengye.service.service.impl.*.*(..))")
    public Object printTime(ProceedingJoinPoint pjp) throws Throwable {
        // 获取方法签名
        MethodSignature methodSignature = getMethodSignature(pjp);

        // 先记录执行目标方法前的时间
        long start = System.currentTimeMillis();
        Object result; // 调用目标方法：例如login方法
        try {
            result = pjp.proceed();
        } finally {
            // 记录执行目标方法后的时间
            long end = System.currentTimeMillis();
            log.warn("\n\n====================耗时统计 Start=================\n" +
                    "Class Method       ：{}.{}\n" +
                    "Method exec time   ：{}毫秒\n" +
                    "====================耗时统计 End=================={}",
                    methodSignature.getDeclaringTypeName(),
                    methodSignature, (end - start),
                    System.lineSeparator());
        }
        return result;
    }

    private MethodSignature getMethodSignature(ProceedingJoinPoint pjp) {
        return (MethodSignature) pjp.getSignature();
    }

    private <T extends Annotation> T getAnnotation(ProceedingJoinPoint pjp, Class<T> clazz) {
        MethodSignature methodSignature = getMethodSignature(pjp);
        return methodSignature.getMethod().getAnnotation(clazz);
    }
}
